﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Diagnostics;

namespace Eventable
{
    /// <summary>
    /// Событийный класс
    /// </summary>
    /// <typeparam name="T">Класс владельца экземпляром провайдера</typeparam>
    public class EventProvider : IEventProvider, IDisposable
    {
        /// <summary>
        /// Ссылки на методы обработчики событий
        /// </summary>
        protected Dictionary<string, EvntHandler> DictEventHands
            = new Dictionary<string, EvntHandler>();


        protected DictionaryList<object, KeyValuePair<string, EvntHandler>> DictLists
            = new DictionaryList<object, KeyValuePair<string, EvntHandler>>();

        /// <summary>
        /// Зарегистрировать события.
        /// </summary>
        /// <param name="nameEvents"></param>
        public void RegisterEvent(params string[] nameEvents)
        {
            if (nameEvents != null)
                foreach (var item in nameEvents)
                    if (!DictEventHands.ContainsKey(item))
                        DictEventHands.Add(item, null);
        }

        /// <summary>
        /// Зарегистрировать события.
        /// </summary>
        /// <param name="nameEvents"></param>
        public void RegisterEvents(IEnumerable<string> nameEvents)
        {
            if (nameEvents != null)
                foreach (var item in nameEvents)
                    if (!DictEventHands.ContainsKey(item))
                        DictEventHands.Add(item, null);
        }

        /// <summary>
        /// Оповестить о событии
        /// </summary>
        /// <param name="nameEvent">Идентификатор события</param>
        /// <param name="args">Параметры события</param>
        /// <returns>Бинарная реакция</returns>
        public bool NotifyEvent(string nameEvent, params object[] args)
        {
            if (DictEventHands.ContainsKey(nameEvent))
                return (DictEventHands[nameEvent] ?? Events.AlwaysTrueEvent)(Owner, nameEvent, args);
            else
                throw new EventNotRegistredException(nameEvent);
        }

        /// <summary>
        /// Оповещение о событии с перехватом исключения (если такое возникнет, то вернуться false в любом случае)
        /// </summary>
        /// <param name="nameEvent">Наименование события</param>
        /// <param name="ex">Перехваченное исключение</param>
        /// <param name="args">Параметры события</param>
        /// <returns>Бинарная реакция</returns>
        public bool TryNotifyEvent(string nameEvent, out Exception ex, params object[] args)
        {
            ex = null;
            try
            {
                return NotifyEvent(nameEvent, args);
            }
            catch (Exception ex1)
            {
                ex = ex1;
            }
            return ex == null;
        }

        /// <summary>
        /// Оповещение о событии c глушением исключения (если такое возникнет, то вернётся false в любом случае)
        /// </summary>
        /// <param name="nameEvent">Наименование события</param>        
        /// <param name="args">Параметры события</param>
        /// <returns>Бинарная реакция</returns>
        public bool TryNotifyEvent(string nameEvent, params object[] args)
        {
            try
            {
                return NotifyEvent(nameEvent, args);
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Оповещение о событии c глушением исключения (если такое возникнет, то вернётся false в любом случае, при этом будут отработаны все делегаты)
        /// </summary>
        /// <param name="nameEvent">Наименование события</param>
        /// <param name="args">Параметры события</param>
        /// <returns>Бинарная реакция</returns>
        public bool TryAllNotifyEvent(string nameEvent, params object[] args)
        {
            bool res = true;
            if (DictEventHands.ContainsKey(nameEvent))
                foreach (var item in (DictEventHands[nameEvent] ?? Events.AlwaysTrueEvent).GetInvocationList().Cast<EvntHandler>())
                    try
                    {
                        res = res & item(Owner, nameEvent, args);
                    }
                    catch
                    {
                        res = false;
                    }
            return res;
        }

        public bool TryAllNotifyEvent(string nameEvent, out Exception[] exs, params object[] args)
        {
            var exl = new List<Exception>();
            bool res = true;
            if (DictEventHands.ContainsKey(nameEvent))
                foreach (var item in (DictEventHands[nameEvent] ?? Events.AlwaysTrueEvent).GetInvocationList().Cast<EvntHandler>())
                    try
                    {
                        res = res & item(Owner, nameEvent, args);
                    }
                    catch (Exception ex)
                    {
                        exl.Add(ex);
                        res = false;
                    }
            else
            {
                exl.Add(new EventNotRegistredException(nameEvent));
                res = false;
            }
            exs = exl.ToArray();
            return res;
        }

        public IEnumerable<string> GetDeclaredEvents()
        {
            return DictEventHands.Keys.ToArray();
        }

        /// <summary>
        /// Установка прослушивания события
        /// </summary>
        /// <param name="evidence">Мандат (разрешение) на прослушивание</param>
        /// <param name="nameEvent">Идентификатор события</param>
        /// <param name="handler">Ссылка на метод обработчик</param>
        public void ListenEvent(object keyObj, EvntHandler handler, params string[] nameEvents)
        {
            if (nameEvents != null)
                foreach (var item in nameEvents)
                    if (DictEventHands.ContainsKey(item))
                        AttachHandle(item, keyObj, handler);
        }

        /// <summary>
        /// Отказ от прослушивания события
        /// </summary>
        /// <param name="nameEvent">Идентификатор события</param>
        /// <param name="handler">Ссылка на метод обработчик</param>
        public void UnlistenEvent(object keyObj, EvntHandler handler, params string[] nameEvents)
        {
            EvntHandler hnd;
            if (nameEvents != null)
                foreach (var item in nameEvents)
                    if (DictEventHands.TryGetValue(item, out hnd))
                    {
                        DictEventHands[item] = (EvntHandler)Delegate.Remove(hnd, handler);
                        if (DictLists.Contains(keyObj))
                            DictLists[keyObj].Remove(new KeyValuePair<string, EvntHandler>(item, handler));
                    }
        }

        /// <summary>
        /// Отказ от прослушивания события
        /// </summary>       
        public void UnlistenEvent(object keyObj, params string[] nameEvents)
        {
            IList<KeyValuePair<string, EvntHandler>> lst = null;
            KeyValuePair<string, EvntHandler> pair;
            if (nameEvents != null && DictLists.TryGetList(keyObj, out lst))
                foreach (var item in nameEvents)
                {
                    pair = lst.FirstOrDefault(s => s.Key == item);
                    EvntHandler hnd;
                    if (!pair.IsDefault() && DictEventHands.TryGetValue(item, out hnd))
                    {
                        DictEventHands[item] = (EvntHandler)Delegate.Remove(hnd, pair.Value);
                        lst.Remove(new KeyValuePair<string, EvntHandler>(item, pair.Value));
                    }
                }
        }

        public void DettachObject(object keyObj)
        {
            EvntHandler eh;
            if (DictLists.Contains(keyObj))
                foreach (var item in DictLists[keyObj]
                                        .GroupBy(s => s.Key, s => s.Value))
                    if (DictEventHands.TryGetValue(item.Key, out eh))
                    {
                        var hnd = item.Aggregate<EvntHandler, Delegate>(eh, Delegate.Remove);
                        DictEventHands[item.Key] = (EvntHandler)hnd;
                    }
        }


        private void RegisterEventAndAttach(string nameEvent, object keyObj, EvntHandler handler)
        {
            if (!DictEventHands.ContainsKey(nameEvent))
                DictEventHands.Add(nameEvent, null);
            AttachHandle(nameEvent, keyObj, handler);
        }
        private void AttachHandle(string nameEvent, object keyObj, EvntHandler handler)
        {
            DictEventHands[nameEvent] += handler;
            if (keyObj != null)
                DictLists.Add(keyObj, new KeyValuePair<string, EvntHandler>(nameEvent, handler));
        }

        public void DettachEvents()
        {
            DictEventHands.Clear();
            DictLists.Clear();
        }

        private void Init(object owner, bool autoBind, params string[] events)
        {
            Owner = owner ?? this;
            if (autoBind)
                AutoBinding();
            RegisterEvent(events);
        }

        private void AutoBinding()
        {
            Type type = (Owner ?? this).GetType();
            RegisterEvents(ReflectionCache.GetOrAddEvents(type));
        }

        //public EventProvider(object owner)
        //    : this(owner, true)
        //{
        //}

        public EventProvider()
            : this(null, true)
        {
        }

        protected virtual object Owner { get; set; }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="owner">Владелец</param>
        /// <param name="events">Список событий</param>
        public EventProvider(object owner, bool autoBind = true, params string[] events)
        {
            Init(owner, autoBind, events);
        }

        public void Dispose()
        {
            DettachEvents();
        }
    } // class EventableObject    

    internal static class ReflectionCache
    {
        public static readonly Dictionary<Type, HashSet<string>> defaultEvents
            = new Dictionary<Type, HashSet<string>>();
        public static readonly Dictionary<Type, Dictionary<string, MethodInfo>> defaultHandles
            = new Dictionary<Type, Dictionary<string, MethodInfo>>();

        private static void Cache(Type type)
        {
            var hs = new HashSet<string>();
            var ds = new Dictionary<string, MethodInfo>();

            defaultEvents.Add(type, hs);
            defaultHandles.Add(type, ds);

            foreach (var item in type.GetAttributes<RegisterEventsAttribute>(true)
                .SelectMany(s => s.NameEvents))
                hs.Add(item);

            foreach (var item in type.GetAttributes<RegisterChangeEventsAttribute>(true)
                .SelectMany(s => s.NameEvents))
                hs.Add(item);

            foreach (var item in type.GetPropertiesAndAttribute<RegisterPropertyChangeAttribute>(true)
                .SelectMany(s => s.Value.GetNamesWith(s.Key.Name)))
                hs.Add(item);

            foreach (var item in type.GetMethodsAndAttribute<RegisterOperationEventsAttribute>(true)
                    .SelectMany(s => s.Value.NameOps))
                hs.Add(item);

            //EventHandlerAttribute eha;
            //ParameterInfo[] pi;
            //foreach (var item in type
            //    .GetMethodsWithAttribute<EventHandlerAttribute>(true)
            //    .Where(s =>
            //        (pi = s.GetParameters()).Length == 3
            //        && pi[0].ParameterType == typeof(object)
            //        && pi[1].ParameterType == typeof(string)
            //        && pi[2].ParameterType == typeof(object[])
            //        && !s.IsStatic))
            //{
            //    eha = item.GetAttribute<EventHandlerAttribute>(true);
            //    if (!eha.HandlerName.IsEmpty())
            //        ds.Add(eha.HandlerName, item);
            //    else
            //        ds.Add(item.Name, item);
            //}
        }

        public static IEnumerable<string> GetOrAddEvents(Type type)
        {
            if (type != null)
            {
                if (!defaultEvents.ContainsKey(type))
                    Cache(type);
                var hs = defaultEvents[type];
                foreach (var item in hs)
                    yield return item;
            }
        }
    }
}
